﻿using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;

using Mono.Collections.Generic;
using NETDeob.Deobfuscators.Generic.Emulation;

namespace NETDeob.Misc
{
    public static class Extensions
    {
        public static Instruction HasDelegateReference(this MethodDefinition mDef, FieldDefinition Delegate)
        {
            return mDef.Body.Instructions.Where(instr => instr.OpCode == OpCodes.Newobj).FirstOrDefault(instr => instr.Operand == Delegate);
        }

        public static EmbeddedResource FindResource(this AssemblyDefinition asmDef, Predicate<Resource> pred)
        {
            return (from modDef in asmDef.Modules from res in modDef.Resources where pred(res) select res as EmbeddedResource).FirstOrDefault();
        }

        public static MethodDefinition FindMethod(this AssemblyDefinition asmDef, Predicate<MethodDefinition> pred)
        {
            return (from modDef in asmDef.Modules from typeDef in modDef.Types from mDef in typeDef.Methods where mDef.HasBody select mDef).FirstOrDefault(mDef => pred(mDef));
        }

        public static List<MethodDefinition> FindMethods(this AssemblyDefinition asmDef, Predicate<MethodDefinition> pred)
        {
            return (from modDef in asmDef.Modules from typeDef in modDef.Types from mDef in typeDef.Methods where mDef.HasBody where pred(mDef) select mDef).ToList();
        }

        public static Collection<Instruction> SplitInstructions(this Collection<Instruction> instructions, int offset)
        {
            var outCollection = new Collection<Instruction>();

            foreach (var instr in instructions.Where(instr => instr.Offset > offset))
                outCollection.Add(instr);

            return outCollection;
        }

        public static T GetOperandAt<T>(this Collection<Instruction> instructions, OpCode opCode, int index)
        {
            for (int i = 0, x = 0; i < instructions.Count; i++)
                if (instructions[i].OpCode == opCode)
                    if (x++ == index)
                        return (T)instructions[i].Operand;

            return default(T);
        }

        public static T GetOperandAt<T>(this Collection<Instruction> instructions, Predicate<Instruction> pred, int index)
        {
            for (int i = 0, x = 0; i < instructions.Count; i++)
                if (pred(instructions[i]))
                    if (x++ == index)
                        return (T)instructions[i].Operand;

            return default(T);
        }

        public static int GetOpCodeCount(this Collection<Instruction> instructions, OpCode opCode)
        {
            if (instructions.Count == 0)
                return 0;

            return instructions.Count(instr => instr.OpCode == opCode);
        }

        public static Collection<Instruction> FindInstructions(this Collection<Instruction> instructions, OpCode[] opCodes)
        {
            var outCollection = new Collection<Instruction>();

            foreach (var instr in instructions.Where(instr => opCodes.Contains(instr.OpCode)))
                outCollection.Add(instr);

            return outCollection;
        }

        public static List<Instruction> GetInstructionBlock(this Collection<Instruction> instructions, int startIndex, Predicate<Instruction> ender)
        {
            var outList = new List<Instruction>();
            var instr = instructions[++startIndex];

            while (!(ender(instr.Next)))
            {
                outList.Add(instr);
                instr = instr.Next;
            }

            outList.Add(instr);

            return outList;
        }

        public static List<Instruction> GetInstructionBlock(this Collection<Instruction> instructions, int startIndex, int count)
        {
            var outList = new List<Instruction>();

            for (var i = startIndex; i < startIndex + count; i++)
                outList.Add(instructions[i]);

            return outList;
        }

        public static int GetInstructionIndex(this Instruction instr, Collection<Instruction> instructions)
        {
            var count = 0;

            while (instructions[count++] != instr)
// ReSharper disable RedundantJumpStatement
                continue;
// ReSharper restore RedundantJumpStatement

            return count;
        }

        public static int GetLdcI4(this Instruction instr)
        {
            switch(instr.OpCode.Code)
            {
                case Code.Ldc_I4_0:
                case Code.Ldc_I4_1:
                case Code.Ldc_I4_2:
                case Code.Ldc_I4_3:
                case Code.Ldc_I4_4:
                case Code.Ldc_I4_5:
                case Code.Ldc_I4_6:
                case Code.Ldc_I4_7:
                case Code.Ldc_I4_8:
                    return Int32.Parse(instr.OpCode.Code.ToString().Split('_')[2]); // Lazy :)

                case Code.Ldc_I4_M1:
                    return -1;

                case Code.Ldc_I4:
                case Code.Ldc_I4_S:
                    return (int) Convert.ChangeType(instr.Operand, typeof (int)); // No idea why I have to cast it this way

                default:
                    throw new Exception("Internal invalid instruction!");
            }
        }
        
        public static IEnumerable<MethodDefinition> GetConstructors(this TypeDefinition typeDef)
        {
            return typeDef.Methods.Where(m => m.IsConstructor);
        }

        public static MethodDefinition GetStaticConstructor(this TypeDefinition typeDef)
        {
            return typeDef.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
        }

        public static bool IsCall(this Instruction instr)
        {
            return instr.OpCode == OpCodes.Call ||
                   instr.OpCode == OpCodes.Calli ||
                   instr.OpCode == OpCodes.Callvirt;
        }

        public static bool IsLdcI4WOperand(this Instruction instr)
        {
            return instr.OpCode == OpCodes.Ldc_I4 ||
                   instr.OpCode == OpCodes.Ldc_I4_S;
        }

        public static bool IsLdcI8WOperand(this Instruction instr)
        {
            return instr.OpCode == OpCodes.Ldc_I8;
        }

        public static bool IsLdcI4(this Instruction instr)
        {
            return instr.OpCode == OpCodes.Ldc_I4 ||
                   instr.OpCode == OpCodes.Ldc_I4_0 ||
                   instr.OpCode == OpCodes.Ldc_I4_1 ||
                   instr.OpCode == OpCodes.Ldc_I4_2 ||
                   instr.OpCode == OpCodes.Ldc_I4_3 ||
                   instr.OpCode == OpCodes.Ldc_I4_4 ||
                   instr.OpCode == OpCodes.Ldc_I4_5 ||
                   instr.OpCode == OpCodes.Ldc_I4_6 ||
                   instr.OpCode == OpCodes.Ldc_I4_7 ||
                   instr.OpCode == OpCodes.Ldc_I4_8 ||
                   instr.OpCode == OpCodes.Ldc_I4_M1 ||
                   instr.OpCode == OpCodes.Ldc_I4_S;
        }
       
        public static Instruction FirstOfOpCode(this Collection<Instruction> instructions, OpCode opCode)
        {
            return instructions.FirstOrDefault(t => t.OpCode == opCode);
        }

        public static Instruction FindInstruction(this Collection<Instruction> instructions, Predicate<Instruction> pred, int index)
        {
            for (int i = 0, idx = 0; i < instructions.Count; i++)
                if (pred(instructions[i]))
                    if(idx++ == index)
                        return instructions[i];

            return null;
        }

        public static Instruction FindInstruction(this Collection<Instruction> instructions, Predicate<Instruction> pred, int index, int start)
        {
            for (int i = start, idx = 0; i < instructions.Count; i++)
                if (pred(instructions[i]))
                    if (idx++ == index)
                        return instructions[i];

            return null;
        }

        public static Instruction FirstOfOpCode(this Collection<Instruction> instructions, Predicate<OpCode> opCode, int index)
        {
            for (int i = 0, x = 0; i < instructions.Count; i++)
                if (opCode(instructions[i].OpCode))
                {
                    if (++x == index)
                        return instructions[i];
                }

            return null;
        }

        public static bool IsTarget(this Instruction instr, MethodBody body)
        {
            return body.Instructions.Where(OnFunc).Any(instr1 => instr1.Operand as Instruction == instr);
        }

        private static bool OnFunc(Instruction instr)
        {
            return instr.IsUnconditionalJump();
        }

        public static bool IsStLoc(this Instruction instr)
        {
            return instr.OpCode == OpCodes.Stloc ||
                   instr.OpCode == OpCodes.Stloc_0 ||
                   instr.OpCode == OpCodes.Stloc_1 ||
                   instr.OpCode == OpCodes.Stloc_2 ||
                   instr.OpCode == OpCodes.Stloc_3 ||
                   instr.OpCode == OpCodes.Stloc_S;

        }

        public static bool IsLdLoc(this Instruction instr)
        {
            return instr.OpCode == OpCodes.Ldloc ||
                   instr.OpCode == OpCodes.Ldloc_0 ||
                   instr.OpCode == OpCodes.Ldloc_1 ||
                   instr.OpCode == OpCodes.Ldloc_2 ||
                   instr.OpCode == OpCodes.Ldloc_3 ||
                   instr.OpCode == OpCodes.Ldloc_S;

        }

        public static bool IsUnconditionalJump(this Instruction instr)
        {
            return
                instr.OpCode == OpCodes.Br ||
                instr.OpCode == OpCodes.Br_S ||
                instr.OpCode == OpCodes.Jmp ||
                instr.OpCode == OpCodes.Leave_S ||
                instr.OpCode == OpCodes.Leave;
        }

        public static bool IsConditionalJump(this Instruction instr)
        {
            return
                instr.OpCode == OpCodes.Brtrue ||
                instr.OpCode == OpCodes.Brtrue_S ||
                instr.OpCode == OpCodes.Brfalse ||
                instr.OpCode == OpCodes.Brfalse_S ||
                instr.OpCode == OpCodes.Ble ||
                instr.OpCode == OpCodes.Ble_S ||
                instr.OpCode == OpCodes.Ble_Un ||
                instr.OpCode == OpCodes.Ble_Un_S ||
                instr.OpCode == OpCodes.Blt ||
                instr.OpCode == OpCodes.Blt_S ||
                instr.OpCode == OpCodes.Blt_Un ||
                instr.OpCode == OpCodes.Blt_Un_S ||
                instr.OpCode == OpCodes.Bge ||
                instr.OpCode == OpCodes.Bge_S ||
                instr.OpCode == OpCodes.Bge_Un ||
                instr.OpCode == OpCodes.Bge_Un_S ||
                instr.OpCode == OpCodes.Beq ||
                instr.OpCode == OpCodes.Beq_S;
        }

        public static string CutString(this string str, int index)
        {
            return str.Substring(10) + "...";
        }

    }
}
